/*
 * Copyright (C) 2012 David Bordoley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package restlib.net;

import java.net.IDN;
import java.net.InetAddress;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;

import javax.annotation.concurrent.NotThreadSafe;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.net.InetAddresses;
import com.google.common.net.InternetDomainName;

/**
 * A builder for minting Tag URI objects. TagBuilder instances can be reused; 
 * it is safe to call build() multiple times to build multiple Tags.
 */
@NotThreadSafe
public final class TagBuilder {
    private static final ThreadLocal<Calendar> CALENDAR = new ThreadLocal<Calendar>() {
        @Override
        protected Calendar initialValue() {
            return Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        }
    };
    
    private String authorityName = "";
    private final UriBuilder builder = Uri.builder().setScheme(UriSchemes.TAG);
    private String date;
    private Path specificPath = Path.of();
    
    TagBuilder(){
        setDate(System.currentTimeMillis());
    }
    
    /**
     * Returns a new tag {@code Uri} instance.
     * @throws IllegalStateException If the Tag instance that would be generated by this 
     * builder is not a syntactically valid URI or if the authority name component
     * of the tag is undefined.
     */
    public Uri build() {
        Preconditions.checkState(!this.authorityName.isEmpty());
        this.builder.setPath(Path.copyOf(getPathSegments()));
        return this.builder.build();
    }
    
    private Iterable<String> getPathSegments(){
        final String pathStart = this.authorityName + "," + this.date + ":";
        if (this.specificPath.isEmpty()) {
            return ImmutableList.of(pathStart);
        } else {
            final List<String> firstSegment = 
                    ImmutableList.of(pathStart + Iterables.getFirst(this.specificPath.segments(), ""));
            if (Iterables.size(this.specificPath.segments()) > 1) {
                return Iterables.concat(
                        firstSegment, 
                        Iterables.skip(this.specificPath.segments(), 1));
            } else {
                return firstSegment;
            }
        }
    }
    
    /**
     * Sets the authority name component of the {@code TagBuilder} from an
     * {@code InetAddress}.
     * 
     * <p> Note: the tag authority name component is a path segment not the 
     * URI authority.
     * @param authorityName any valid IPv4 or IPv6 address.
     * @return this {@code TagBuilder} instance.
     * @throws NullPointerException if {@code authorityName} is null.
     */
    public TagBuilder setAuthorityName(final InetAddress authorityName) {
        setAuthorityName(InetAddresses.toUriString((InetAddress) authorityName));
        return this;
    }
    
    /**
     * Sets the authority name component of the {@code TagBuilder} from an
     * {@code InternetDomainName}.
     * 
     * <p> Note: the tag authority name component is a path segment not the 
     * URI authority.
     * @param authorityName any valid Internet domain name.
     * @return this {@code TagBuilder} instance.
     * @throws NullPointerException if {@code authorityName} is null.
     */
    public TagBuilder setAuthorityName(final InternetDomainName authorityName) {
        setAuthorityName(IDN.toASCII(authorityName.name()));
        return this;
    }    
    
    /**
     * Sets the authority name component of the {@code TagBuilder}.
     * 
     * <p> Note: the tag authority name component is a path segment not the 
     * URI authority.
     * @param authorityName a URI encoded authority name string.
     * @return this {@code TagBuilder} instance.
     * @throws NullPointerException if {@code authorityName} is null.
     * @throws IllegalArgumentException if {@code authorityName} is not a valid
     * URI encoded Tag authority name.
     */
    public TagBuilder setAuthorityName(final String authorityName) {
        Preconditions.checkNotNull(authorityName);
        Preconditions.checkArgument(IRIPredicates.IS_SEGMENT.apply(authorityName));
        this.authorityName = authorityName;
        return this;
    }
    
    private TagBuilder setDate(final Calendar calendar){
        return setDate(
                calendar.get(Calendar.YEAR),
                calendar.get(Calendar.MONTH) + 1,
                calendar.get(Calendar.DATE));
    }
    
    /**
     * Sets the date that {@code Tag}'s built by this {@code TagBuilder} will be minted on.
     * @param year the 4 digit year the tag should be minted on.
     * @return this {@code TagBuilder} instance.
     * @throws IllegalArgumentException if year is not a valid 4 digit year,.
     */
    public TagBuilder setDate(final int year) {
        return setDate(year, -1, -1);
    }
    
    /**
     * Sets the date that {@code Tag}'s built by this {@code TagBuilder} will be minted on.
     * @param year the 4 digit year the tag should be minted on.
     * @param month the 2 digit month the tag should be minted on.
     * @return this {@code TagBuilder} instance.
     * @throws IllegalArgumentException if year is not a valid 4 digit year, or 
     * if month is not a valid 2 digit month.
     */
    public TagBuilder setDate(final int year, final int month) {
        return setDate(year, month, -1);
    }
    
    /**
     * Sets the date that {@code Tag}'s built by this {@code TagBuilder} will be minted on.
     * @param year a 4 digit year the tag should be minted on.
     * @param month a 2 digit month the tag should be minted on, or -1 to indicate no month.
     * @param day a 2 digit day the tag should be minted on or -1 to indicate no day.
     * @return this {@code TagBuilder} instance.
     * @throws IllegalArgumentException if year is not a valid 4 digit year, or 
     * if month is not a valid 2 digit month, or if day is not a valid 2 digit day.
     */
    public TagBuilder setDate(final int year, final int month, final int day) {
        Preconditions.checkArgument((year >= 0) && (year <= 9999));
        Preconditions.checkArgument(((month >= 1) && (month <= 12)) || (month == -1));
        Preconditions.checkArgument( 
                (month == -1) ? (day == -1) : (((day >= 1) && (day <= 31)) || (day == -1)));
        
        final StringBuilder builder = new StringBuilder(10);
        builder.append(year);
    
        if (month > 0) {
            builder.append('-')
                    .append(Strings.padStart(Integer.toString(month), 2, '0'));
        }
        
        if (day > 0) {
            builder.append('-')
                    .append(Strings.padStart(Integer.toString(day), 2, '0'));
        }
 
        this.date = builder.toString();
        return this;
    }
    
    /**
     * Sets the date that the {@code Uri} built by this {@code TagBuilder} will be minted on.
     * @param datetime the number of milliseconds since January 1, 1970, 00:00:00 GMT.
     * @return This {@code TagBuilder} instance.
     */
    public TagBuilder setDate(final long datetime){
        final Calendar calendar = CALENDAR.get();
        calendar.clear();
        
        calendar.setTimeInMillis(datetime);
        return setDate(calendar);
    }
    
    /**
     * Sets the fragment component of the {@code TagBuilder}.
     * @param fragment a URI encoded fragment string.
     * @return this {@code TagBuilder} instance.
     * @throws NullPointerException if {@code fragment} is null.
     * @throws IllegalArgumentException if {@code fragment} contains
     * characters not allowed in the URI fragment component.
     */
    public TagBuilder setFragment(final String fragment) {
        this.builder.setFragment(fragment);
        return this;
    }

    /**
     * Sets the specific path component of the {@code TagBuilder}.
     * @param path a {@code Path} instance that is a valid URI path.
     * @return this {@code TagBuilder} instance.
     * @throws NullPointerException if {@code path} is null.
     * @throws IllegalArgumentException if {@code path} is not a valid URI path.
     */
    public TagBuilder setSpecificPath(final Path path) {
        Preconditions.checkNotNull(path);
        Preconditions.checkArgument(path.isUriPath());
        this.specificPath = path;
        return this;
    }
    
    /**
     * Sets the specific path component of the {@code TagBuilder}.
     * @param path a {@code String} instance that is a valid URI path.
     * @return this {@code TagBuilder} instance.
     * @throws NullPointerException if {@code path} is null.
     * @throws IllegalArgumentException if {@code path} is not a valid URI path.
     */
    public TagBuilder setSpecificPath(final String path) {
        return setSpecificPath(Path.parse(path));
    }
    
    /**
     * Sets the specific query component of the {@code TagBuilder}.
     * @param query A URI encoded query string.
     * @return this {@code TagBuilder} instance.
     * @throws NullPointerException if {@code query} is null.
     * @throws IllegalArgumentException if {@code query} contains
     * characters not allowed in the URI query component.
     */
    public TagBuilder setSpecificQuery(final String query) {
        this.builder.setQuery(query);
        return this;
    }
}
